跳到主要内容

GNU linker

绪论

ld组合了许多对象和归档文件(object and archive files),重新定位它们的数据并绑定符号引用。通常编译程序的最后一步是运行ld。

ld接受用AT&T的链接编辑器命令语言语法的超集编写的链接器命令语言文件,以提供对链接过程的显式和全面控制。

这个版本的ld使用通用的BFD库对目标文件进行操作。这允许ld以多种不同的格式(例如COFF或a.out)读取、组合和写入对象文件。可以将不同的格式链接在一起以生成任何可用的对象文件。有关更多信息,请参见BFD。

除了灵活性之外,GNU链接器在提供诊断信息方面比其他链接器更有帮助。 许多链接器在遇到错误时立即放弃执行;只要可能,ld就会继续执行,允许您识别其他错误(或者,在某些情况下,尽管存在错误,也可以获得输出文件)。

Linker脚本

每个链接都由链接器脚本控制。这个脚本是用链接器命令语言编写的。

链接器脚本的主要目的是描述如何将输入文件中的节映射到输出文件中,并控制输出文件的内存布局。大多数链接器脚本都是这样做的。但是,必要时,链接器脚本还可以使用下面描述的命令指导链接器执行许多其他操作。

链接器总是使用链接器脚本。如果您自己不提供,链接器将使用编译到链接器可执行文件中的默认脚本。您可以使用“--verbose”命令行选项来显示默认的链接器脚本。某些命令行选项,如“-r”或“-N”,将影响默认的链接器脚本。

您可以使用“-T”命令行选项提供自己的链接器脚本。当您这样做时,您的链接器脚本将替换默认的链接器脚本。

您还可以隐式地使用链接器脚本,将它们命名为链接器的输入文件,就好像它们是要链接的文件一样。参见隐式链接器脚本( Implicit Linker Scripts)。

基本的链接器脚本概念

为了描述链接器脚本语言,我们需要定义一些基本概念和词汇表。

链接器将输入文件组合成单个输出文件。 输出文件和每个输入文件采用一种称为目标文件格式(object file format)的特殊数据格式。每个文件称为一个目标文件。输出文件通常称为可执行文件,但出于我们的目的,我们也将其称为目标文件。除其他外,每个对象文件都有一个section列表(a list of sections)。我们有时将输入文件中的一个section称为输入section;类似地,输出文件中的section也是输出section。

对象文件中的每个section都有名称和大小。大多数section还有一个相关的数据块,称为section contents。一个section可以标记为loadable,这意味着在运行输出文件时应该将内容加载到内存中。没有内容的部分可能是可分配的(allocatable),这意味着应该留出内存中的某个区域,但不应该在那里加载任何内容(在某些情况下,必须将该内存归零)。既不可加载又不可分配的部分通常包含某种调试信息。

每个可加载(loadable)或可分配(allocatable)的输出部分都有两个地址。第一个是VMA,即虚拟内存地址(virtual memory address)。这是该section在运行输出文件时的地址。第二个是LMA,即加载内存地址(load memory address)。这是加载该section的地址。在大多数情况下,这两个地址是相同的。数据段加载到[存储到;loaded into]ROM中,然后在程序启动时复制到RAM中(这种技术通常用于在基于ROM的系统中初始化全局变量),这就是它们可能有所不同的一个例子。在这种情况下,ROM地址将是LMA, RAM地址将是VMA。

通过使用带有“-h”选项的objdump程序,可以看到对象文件中的各个部分。

每个对象文件还有一个符号列表,称为符号表。符号可以定义,也可以不定义。在其他信息中,每个符号都有一个名称,每个定义的符号都有一个地址。如果将C或C++程序编译为对象文件,则会为每个已定义的函数和全局或静态变量获得一个已定义的符号。输入文件中引用的每个未定义的函数或全局变量都将成为未定义的符号。

您可以通过使用nm程序或使用带有“-t”选项的objdump程序来查看对象文件中的符号。

链接器脚本格式

链接器脚本是文本文件。

将链接器脚本编写为一系列命令。每个命令要么是关键字(可能后面跟着参数),要么是符号的赋值。可以使用分号分隔命令。空格通常被忽略。

通常可以直接输入文件或格式名称等字符串。如果文件名包含逗号等字符,否则将用于分隔文件名,则可以将文件名放在双引号中。无法在文件名中使用双引号字符。

您可以在链接器脚本中添加注释,就像在C中一样,用“/”和“/”分隔。在C语言中,注释在语法上等同于空格。

简单的链接器脚本示例

许多链接器脚本相当简单。

最简单的链接器脚本只有一个命令:“SECTIONS”。您可以使用“SECTIONS”命令来描述输出文件的内存布局。‘SECTIONS’命令是一个强大的命令。这里我们将描述它的一个简单用法。让我们假设您的程序只包含代码、初始化数据和未初始化数据。这些分别会在'.text'、 '.data '、'.bss' 。让我们进一步假设输入文件仅有这些段。

SECTIONS
{
. = 0x10000;
.text : { *(.text) }
. = 0x8000000;
.data : { *(.data) }
.bss : { *(.bss) }
}

您可以将' SECTIONS '命令编写为关键字' SECTIONS ',后面是一系列符号赋值和用大括号括起来的输出段描述。

上面示例的“SECTIONS”命令中的第一行设置了特殊符号.的值,这是 location counter。如果没有以其他方式指定输出段的地址(稍后将描述其他方式),则从 location counter的当前值设置地址。然后 location counter由输出段的大小递增。 在“SECTIONS”命令的开头, location counter的值为“0”。

第二行定义了输出段“.text”。冒号是必需的语法,暂时可以忽略它。在输出段名称后面的大括号中,列出了应该放在该输出段中的输入段的名称。*是匹配任何文件名的通配符。表达式“*(.text)”表示所有输入文件中的.text段。

因为输出段.text的位置计数器是0x10000 。链接器将设置输出文件中的.text段的地址为0x10000

其余的行定义了输出文件中的.data.bss段,链接器在地址0x8000000处放置.data输出段。链接器放置好.data输出段后,位置计数器的值为0x8000000加上.data输出段的大小的结果值。其效果是链接器紧接着在.data段后面放置.bss段。

连接器将通过在必要时增加位置计数器来确保每个输出段具有所需的对齐。 在本例中,为.text.data指定存放地址,将可以满足任何对齐约束,但链接器可能必须在.data.bss之间创建一个小的间隙(gap)以满足对齐约束。

就是这样!这是一个简单而完整的链接器脚本。

简单的链接器脚本命令

3.4.1 设置入口点

在程序中执行的第一条指令称为入口点(entry point)。您可以使用ENTRY链接器脚本命令来设置入口点。参数是一个符号名:

ENTRY(symbol)

有几种方法可以设置入口点。链接器将通过依次尝试以下每一种方法来设置入口点,并在其中一种方法成功时停止:

  • 输入命令行选项-e;
  • 链接器脚本中的ENTRY(symbol)命令;
  • 目标特定符号的值(如果已定义);对于许多目标,这是start,但是基于PE和BeOS的系统会检查可能的输入符号列表,匹配找到的第一个。
  • .text段的第一个字节的地址
  • 地址0

3.6 SECTIONS 命令

SECTIONS命令告诉链接器如何将输入段映射到输出段,以及如何将输出段放在内存中。

SECTIONS命令的格式为:

SECTIONS
{
sections-command
sections-command

}

每个sections-command可以是下列命令之一:

  • ENTRY命令
  • 符号赋值(参见赋值)
  • 输出段描述
  • 一个overlay描述

SECTIONS命令中允许ENTRY命令和符号分配,以便在这些命令中使用位置计数器。这还可以使链接器脚本更容易理解,因为您可以在输出文件的布局中有意义的地方使用这些命令。

输出段描述和覆盖描述如下所述。

如果在链接器脚本中不使用SECTIONS命令,链接器将把每个输入段按输入文件中首次遇到的段的顺序放置到同名的输出部分中。 例如,如果第一个文件中出现了所有输入段,那么输出文件中各段的顺序将与第一个输入文件中的顺序匹配。第一个段位于地址0。

输出段描述

输出段的完整描述如下:

section [address] [(type)] :
[AT(lma)]
[ALIGN(section_align) | ALIGN_WITH_INPUT]
[SUBALIGN(subsection_align)]
[constraint]
{
output-section-command
output-section-command

} [>region] [AT>lma_region] [:phdr :phdr …] [=fillexp] [,]

大多数输出段不使用大多数可选段属性。

section周围的空格是必需的,因此section名称是明确的。冒号和花括号也是必需的。如果使用fillexp,并且下一个sections-command看起来像表达式的延续,那么末尾的逗号可能是必需的。换行符和其他空白是可选的。

输出段名字

输出段的名称是段.section。段必须满足输出格式的约束。一些格式只支持有限数量的段,例如a.out。,名称必须是格式支持的名称之一。例如,a.out只允许.text.data或者.bss。如果输出格式支持任意数量的段,但是只有数字而没有名称(就像Oasys的情况一样),则名称应该作为带引号的数字字符串提供。段名可以由任何字符序列组成,但是包含任何不寻常字符(如逗号)的名称必须加引号。

输出段/DISCARD/比较特殊。

3.6.3 输出段地址

address是输出段的VMA(虚拟内存地址)的表达式。此地址是可选的,但如果提供了此地址,则输出地址将完全按照指定设置。

如果没有指定输出地址,那么将根据下面的推导有一个被该段所选择。此地址将被调整以适应输出段的对齐要求。对齐要求是输出段中包含的任何输入段的最严格对齐。

输出段地址启发式如下:

  • 如果为该区段设置了输出内存区域,则将其添加到该区域,其地址将是该区域中的下一个空闲地址。

  • 如果已使用MEMORY命令创建内存区域列表,则选择具有与该段兼容的属性的第一个区域来包含该区域。该段的输出地址将是该区域中的下一个空闲地址

  • 如果没有指定内存区域,或者没有与该段匹配,那么输出地址将基于位置计数器的当前值。

例如:

.text . : { *(.text) }

.text : { *(.text) }

有微妙的不同。第一个将设置.text输出段的地址。到位置计数器的当前值。 第二个将把它设置为与任何.text输入段的输入段中最严格对齐的位置计数器的当前值。

address可以是任意表达式;例如,如果您想在0x10字节的边界上对齐某段,以便该段地址的最低四位为零,您可以这样做:

.text ALIGN(0x10) : { *(.text) }

这是因为ALIGN返回向上(意即后面)对齐到指定值的当前位置计数器。

为某段指定address将更改location counter的值,前提是该段是非空的。(空段除外)。

3.6.4.3 Input Section for Common Symbols

公共符号需要特殊的符号,因为在许多对象文件格式中,公共符号没有特定的输入部分。 链接器将公共符号视为在名为“common”的输入段中的公共符号。

您可以在“COMMON”部分中使用文件名,就像在任何其他输入段中一样。 您可以使用它将来自特定输入文件的公共符号放在一个段中,而将来自其他输入文件的公共符号放在另一个段中。

在大多数情况下,输入文件中的公共符号将放在输出文件的.bss段中。例如:

.bss { *(.bss) *(COMMON) }

一些目标文件格式具有不止一种类型的公共符号。例如,MIPS ELF对象文件格式区分标准通用符号和小通用符号。在这种情况下,链接器将为其他类型的公共符号使用不同的特殊节名。 在MIPS ELF的情况下,链接器使用“COMMON”作为标准的公共符号和.scommon是指普通的小符号。 这允许您将不同类型的公共符号映射到不同位置的内存中。

您有时会在旧的链接器脚本中看到[COMMON]这个符号现在被认为过时了。它相当于*(COMMON)

3.6.4.4 Input Section and Garbage Collection

在使用链接时垃圾收集('--gc-sections '),标记不应该删除的段通常很有用。 这是通过使用KEEP()包围输入部分的通配符条目来实现的,就像在KEEP(*(.init))或KEEP(SORT_BY_NAME(*)(.ctors)中那样。

Output Section LMA

每个段都有一个虚拟地址(VMA)和一个加载地址(LMA);虚拟地址由前面描述的输出段地址指定。加载地址由ATAT>关键字指定。指定加载地址是可选的。

AT关键字以表达式为参数。这指定了该段的准确加载地址。AT>关键字以内存区域(memory region)的名称作为参数。section的加载地址设置为区域中的下一个空闲地址,与section的对齐要求对齐。

如果没有为可分配段指定AT或AT>,则链接器将使用以下推导式方法确定加载地址:

  • 如果这个段有一个特定的VMA地址,那么它也可以用作LMA地址。
  • 如果段不可分配,则将其LMA设置为其VMA。
  • 否则如果能够找到与当前段兼容的memory region,并且该region至少包含一个段,然后设置LMA,使VMA与LMA的差值等于上一段VMA与LMA在此region定位的差值。
  • 如果没有声明内存区域,则在前一步中使用覆盖整个地址空间的默认区域。
  • 如果找不到合适的区域,或者之前没有section,那么LMA就等于VMA。

这个特性的设计目的是使构建ROM映像变得容易。例如,下面的链接器脚本创建三个输出部分:一个名为.text,从0x1000开始,其中一个名为.mdata存放在.text段的末尾,尽管它的VMA是0x2000,还有一个名为.bss段在地址0x3000处保存未初始化的数据。符号_data是用值0x2000定义的,这表明位置计数器保存的是VMA值,而不是LMA值。

SECTIONS
{
.text 0x1000 : { *(.text) _etext = . ; }
.mdata 0x2000 :
AT ( ADDR (.text) + SIZEOF (.text) )
{ _data = . ; *(.data); _edata = . ; }
.bss 0x3000 :
{ _bstart = . ; *(.bss) *(COMMON) ; _bend = . ;}
}

使用这个链接器脚本生成的程序使用的运行时初始化代码将包括如下内容,将初始化的数据从ROM映像复制到其运行时地址。注意这段代码如何利用链接器脚本定义的符号。

extern char _etext, _data, _edata, _bstart, _bend;
char *src = &_etext;
char *dst = &_data;

/* ROM has data at end of text; copy it. */
while (dst < &_edata)
*dst++ = *src++;

/* Zero bss. */
for (dst = &_bstart; dst< &_bend; dst++)
*dst = 0;

Output Section Region

您可以使用“>region”将一个段分配给先前定义的内存区域。

这里有一个简单的示例:

MEMORY { rom : ORIGIN = 0x1000, LENGTH = 0x1000 }
SECTIONS { ROM : { *(.text) } >rom }

MEMORY 命令

链接器的默认配置允许分配所有可用内存。您可以使用MEMORY命令来覆盖它。

MEMORY命令描述目标内存块的位置和大小。您可以使用它来描述链接器可能使用哪些内存区域,以及必须避免哪些内存区域。然后可以为特定的内存区域分配区域。链接器将根据内存区域设置段地址,并警告区域变得太满。链接器不会将各个段重新放置以适应可用的区域。

链接器脚本可能包含内存命令的许多用途,但是,所有定义的内存块都被视为在单个MEMORY命令中指定的。MEMORY的语法是:

MEMORY
{
name [(attr)] : ORIGIN = origin, LENGTH = len

}

name是链接器脚本中用于引用该区域的名称。区域名称在链接器脚本之外没有意义。区域名称存储在单独的名称空间中,不会与符号名称、文件名或节名称冲突。在MEMORY命令中,每个内存区域必须有一个不同的名称。但是,您可以使用REGION_ALIAS命令将以后的别名添加到现有的内存区域。

attr字符串是一个可选的属性列表,它指定是否对链接器脚本中没有显式映射的输入部分使用特定的内存区域。如SECTIONS中所述,如果您没有为某些输入段指定输出段,链接器将创建与输入段同名的输出段。如果您定义了区域属性,链接器将使用它们为它创建的输出段选择内存区域。

attr字符串必须只包含以下字符:

R 只读段

W 可读可写段

X 执行段

A 可分配段

I 初始段

L 和I一样

!反转后面任何属性的意义

如果未映射的部分匹配除!的属性,它将被放置在内存区域。属性!反转后面字符的测试,因此只有在未映射的部分与后面列出的任何属性不匹配时,才会将其放置在内存区域中。因此,属性字符串“RW!X”将匹配具有“R”和“W”属性之一或两者兼有的任何未映射部分,但前提是该部分不具有“X”属性。

origin是内存区域的起始地址的数值表达式。表达式必须求值为常数,不能包含任何符号。关键字ORIGIN可以缩写为org或o(但不能是ORG)。

len是内存区域字节大小的表达式。对于origin表达式,表达式必须仅为数值形式,并且必须计算为常数。关键字LENGTH可以缩写为len或l。

在下面的示例中,我们指定有两个可用的内存区域可供分配:一个从' 0 '开始分配256 kb,另一个从' 0x40000000 '开始分配4 mb。链接器将把没有显式映射到内存区域,并且是只读或可执行的每个部分放入“rom”内存区域。 链接器将把没有显式映射到内存区域,并且是只读或可执行的每个段放入“rom”内存区域。链接器将把没有显式映射到内存区域的其他部分放入“ram”内存区域。

MEMORY
{
rom (rx) : ORIGIN = 0, LENGTH = 256K
ram (!rx) : org = 0x40000000, l = 4M
}

一旦您定义了一个内存区域,您可以通过使用' >region' 输出段属性来指示链接器将特定的输出区域放置到该内存区域中。例如,如果您有一个名为“mem”的内存区域,那么您将在输出段定义中使用“>mem”。如果没有为输出段指定地址,链接器将把该地址设置为内存区域中的下一个可用地址。如果指向内存区域的组合输出段对于该区域来说太大,链接器将发出错误消息。

可以通过ORIGIN(memory)和LENGTH(memory)函数来访问表达式中内存的origin和length:

  _fstack = ORIGIN(ram) + LENGTH(ram) - 4;